#include "SocketSelectDemo.h"
#include <iostream>
#include <list>
#include "ThreadPool.h"

using namespace std;
using namespace common;

void simpleSelectDemo()
{
#ifdef _WIN32
    // windows 下需要装载网络访问库
    WSADATA lpWSAData;
    WSAStartup(MAKEWORD(2, 2), &lpWSAData);
#endif

    // 1. 创建 Socket
    SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket == -1)
    {
        cerr << "创建失败！！" << endl;
        return;
    }

    // 2. bind IP端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(9999);
    int bindRet = ::bind(serverSocket, (sockaddr *)&serverAddr, sizeof(serverAddr));
    if (bindRet == -1)
    {
        cerr << "绑定端口失败！！！" << endl;
        compatCloseSocket(serverSocket);
        return;
    }

    // 3. listen
    int listenRet = listen(serverSocket, 1024);
    if (listenRet == -1)
    {
        cerr << "监听失败！！！" << endl;
        compatCloseSocket(serverSocket);
        return;
    }

    // 4. 使用 select 处理 接受链接、读取数据、写入数据等操作
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(serverSocket, &readfds);
    SOCKET maxSocket = serverSocket;

    while (true)
    {
        // readfds 用于保存要检测的原始数据，这个变量不能作为参数传递给 select 函数，因为在函数内部这个变量中的值会被内核修改，
        // 函数调用完毕返回之后，里边就不是原始数据了，大部分情况下是值为1的标志位变少了，不可能每一轮检测，所有的文件描述符都是就行的状态。
        // 因此需要通过 tmpReadfds 变量将原始数据传递给内核，select() 调用完毕之后再将内核数据传出，这两个变量的功能是不一样的。
        fd_set tmpReadfds = readfds;
        int count = select(maxSocket + 1, &tmpReadfds, NULL, NULL, NULL);
        if (count == -1)
        {
            cerr << "select 异常" << endl;
            compatCloseSocket(serverSocket);
            return;
        }

        if (count > 0)
        {
            // 如果 count 大于零说明有链接或者发送数据要处理，count 表示需要处理得 fd 文件描述符个数。

            // 5. 处理接受 accept
            if (FD_ISSET(serverSocket, &tmpReadfds))
            {
                sockaddr_in clientAddr;
                // unsigned int clientAddrLength = sizeof(clientAddr);
                addrlen clientAddrLength = sizeof(clientAddr);
                SOCKET clientSocket = accept(serverSocket, (sockaddr *)&clientAddr, &clientAddrLength);

                // FD_ISSET 会判定 select 后得结果，如果 serverSocket 对应得标记位有连接过来得客户端那么返回 1，否则返回 0。
                // 所以进入这里基本上就可以判定为用于监听得文件标识符 fd（就是 clientSocket）一定是有值的，既 clientSocket > 0
                // 所以只需要处理 clientSocket == -1 的情况
                if (clientSocket == -1)
                {
                    cerr << "接受客户端异常！！！" << endl;
                    // compatCloseSocket(serverSocket);
                    // return;
                    break;
                }

                // if(clientSocket > 0)
                // {
                //     FD_SET(clientSocket, &readfds);
                //     maxSocket = maxSocket < clientSocket ? clientSocket : maxSocket;
                // }

                FD_SET(clientSocket, &readfds);
                maxSocket = maxSocket < clientSocket ? clientSocket : maxSocket;

                cout << "客户端IP：" << inet_ntoa(clientAddr.sin_addr) << "；端口：" << ntohs(clientAddr.sin_port) << "已连接" << endl;
            }

            // 6. 处理接受数据
            for (SOCKET s = 0; s < maxSocket + 1; ++s)
            {
                if (s != serverSocket && FD_ISSET(s, &tmpReadfds))
                {
                    char readBuff[1024];
                    int len = recv(s, readBuff, sizeof(readBuff), 0);
                    if (len == -1)
                    {
                        // 读取失败
                        cerr << "读取失败异常: " << endl;
                        continue;
                    }
                    else if (len == 0)
                    {
                        // 断开链接
                        cout << "服务器断开链接！" << endl;
                        FD_CLR(s, &readfds);
                        compatCloseSocket(s);
                        continue;
                    }
                    else
                    {
                        // 读取成功
                        cout << "服务器接受到数据: " << readBuff << endl;
                        // 回复给客户端
                        for (int i = 0; i < len; ++i)
                        {
                            readBuff[i] = toupper(readBuff[i]);
                        }
                        int sendRet = send(s, readBuff, strlen(readBuff) + 1, 0);
                        if (sendRet == -1)
                        {
                            // 读取失败
                            cerr << "回复失败: " << endl;
                        }
                    }
                }
            }
        }
    }

    compatCloseSocket(serverSocket);

#ifdef _WIN32
    // windows 平台下结束了还需要移除网络库的装载
    WSACleanup();
#endif
}

void threadPoolSelectDemo()
{
#ifdef _WIN32
    // windows 下需要装载网络访问库
    WSADATA lpWSAData;
    WSAStartup(MAKEWORD(2, 2), &lpWSAData);
#endif

    // 1. 创建 Socket
    SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket == -1)
    {
        cerr << "创建失败！！" << endl;
        return;
    }

    // 2. bind IP端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(9999);
    int bindRet = ::bind(serverSocket, (sockaddr *)&serverAddr, sizeof(serverAddr));
    if (bindRet == -1)
    {
        cerr << "绑定端口失败！！！" << endl;
        compatCloseSocket(serverSocket);
        return;
    }

    // 3. listen
    int listenRet = listen(serverSocket, 1024);
    if (listenRet == -1)
    {
        cerr << "监听失败！！！" << endl;
        compatCloseSocket(serverSocket);
        return;
    }

    // 4. 使用 select 处理 接受链接、读取数据、写入数据等操作
    list<SOCKET> socketList;
    socketList.push_back(serverSocket);
    SOCKET maxSocket = serverSocket;
    
    ThreadPool threadPool(3, 10);
    static mutex selectMutex;

    struct SelectInfo {
        SOCKET tagSocket;
        SOCKET* maxSocket;
        list<SOCKET>* socketList;
    };

    // 5. 处理接受 accept
    TaskCallBack acceptTask = [](void *const arg) -> void {
        SelectInfo* selectInfo = (SelectInfo*) arg;

        sockaddr_in clientAddr;
        addrlen clientAddrLength = sizeof(clientAddr);
        SOCKET clientSocket = accept(selectInfo->tagSocket, (sockaddr *)&clientAddr, &clientAddrLength);

        // FD_ISSET 会判定 select 后得结果，如果 serverSocket 对应得标记位有连接过来得客户端那么返回 1，否则返回 0。
        // 所以进入这里基本上就可以判定为用于监听得文件标识符 fd（就是 clientSocket）一定是有值的，既 clientSocket > 0
        // 所以只需要处理 clientSocket == -1 的情况
        if (clientSocket == -1)
        {
            cerr << "接受客户端异常！！！\n";
            free(selectInfo);
            return;
        }
        
        cout << "客户端IP：" + string(inet_ntoa(clientAddr.sin_addr)) + "；端口：" + to_string(ntohs(clientAddr.sin_port)) + "已连接\n";

        selectMutex.lock();
        selectInfo->socketList->push_back(clientSocket);
        if(clientSocket > *selectInfo->maxSocket)
            *selectInfo->maxSocket = clientSocket;
        selectMutex.unlock();

        free(selectInfo);
    };
    
    // 6. 处理消息数据
    TaskCallBack readMsgTask = [](void *const arg) -> void {
        SelectInfo* selectInfo = (SelectInfo*) arg;

        char readBuff[1024];
        int len = recv(selectInfo->tagSocket, readBuff, sizeof(readBuff), 0);
        if (len == -1)
        {
            // 读取失败
            cerr << "读取失败异常！！！\n";
            compatCloseSocket(selectInfo->tagSocket);
            free(selectInfo);
            return;
        }
        else if (len == 0)
        {
            // 断开链接
            cout << "服务器断开链接！\n";
            compatCloseSocket(selectInfo->tagSocket);
            free(selectInfo);
            return;
        }

        // 读取成功
        cout << "服务器接受到数据: " + string(readBuff) + "\n";

        // 回复给客户端
        for (int i = 0; i < len; ++i)
        {
            readBuff[i] = toupper(readBuff[i]);
        }
        int sendRet = send(selectInfo->tagSocket, readBuff, strlen(readBuff) + 1, 0);
        if (sendRet == -1)
        {
            // 读取失败
            cerr << "回复失败!!!\n";
        }
        
        free(selectInfo);

        selectMutex.lock();
        selectInfo->socketList->push_back(selectInfo->tagSocket);
        selectMutex.unlock();
    };

    fd_set readfds;
    const timeval timeout = { 0, 0 };
    while (true)
    {
        // select 的检查结果真的只受文件描述符指向可读区里是否有内容，只要有内容就返回就绪状态，这里就有个很严重的问题，如果 for 循环开了一个线程去处理读取内存消息操作，
        // 那线程可能并不会马上执行，但是此时的select线程会立马进入下一个循环，检查后发现任然时可读状态，就会有开启一个线程去处理。这样就导致同一个读取状态被开启多个线程来处理。
        // 所以下一次循环的 select 应该排除那些已经开启线程准备去读取的文件操作符。
        // 所以解决方案如下，使用 list 容器来替代之前的拷贝，每次都从 list 容器中设置，这样在读取操作前删除对于的文件描述符，在读取结束后恢复即可。
        selectMutex.lock();
        FD_ZERO(&readfds);
        for(auto item = socketList.begin(); item != socketList.end(); ++item)
        {
            FD_SET(*item, &readfds);
        }
        selectMutex.unlock();

        // 注意：由于已经删除了准备读取的文件描述符，所以剩下的文件描述符可能不存在任何就绪状态，所以这里不能在用 NULL 了，否则可能阻塞致死，除非有新的链接接入。
        // 这里给定一个超时实现，这里我用了 0，立马返回。
        int count = select(maxSocket + 1, &readfds, NULL, NULL, (const PTIMEVAL)&timeout);
        if (count == -1)
        {
            cerr << "select 异常!!!\n";
            compatCloseSocket(serverSocket);
            return;
        }

        if (count > 0)
        {
            // 如果 count 大于零说明有链接或者发送数据要处理，count 表示需要处理得 fd 文件描述符个数。
            if (FD_ISSET(serverSocket, &readfds))
            {
                SelectInfo* selectInfo = (SelectInfo*) malloc(sizeof(SelectInfo));
                selectInfo->tagSocket = serverSocket;
                selectInfo->maxSocket = &maxSocket;
                selectInfo->socketList = &socketList;
                // threadPool.addTask(acceptTask, selectInfo);
                
                // 这个地方如果把 acceptTask 放到子线程执行，那么会导致下面的 for 循环在执行的时候可能 acceptTask 还没有获得时间片执行。
                // 这样就导致了一个结果就是下面的 for 循环的 maxSocket 没有记录更新到最新的文件描述符，导致循环时只有服务的监听的文件描述符。
                // 这样就会错过一条客户端发送的消息。而客户端如果事需要等待客户端返回后在发送下一条请求的话，程序就陷入互相等待了：
                // 因为服务器没有处理第一条，而客户端在等待第一条结果，这种局面需要保证下面的 for 循环必须在 acceptTask 执行后才开始，
                // 所以将 acceptTask 放入从线程处理并不是一个好的做法，其实相反它并不会因为线程增加而增加性能，而会因为加锁和SelectInfo对象的内存操作更加缓慢
                // acceptTask 在已知就绪状态下，操作其实非常快，因为它并没有那么多数据要处理。

                // 这个地方简单修复以下，不应该这样调用，但是这里不想改了。
                acceptTask(selectInfo);
            }
            
            for (SOCKET tagSocket = 0; tagSocket < maxSocket + 1; ++tagSocket)
            {
                if (tagSocket != serverSocket && FD_ISSET(tagSocket, &readfds))
                {
                    selectMutex.lock();
                    socketList.remove(tagSocket);
                    selectMutex.unlock();

                    SelectInfo* selectInfo = (SelectInfo*) malloc(sizeof(SelectInfo));
                    selectInfo->tagSocket = tagSocket;
                    // selectInfo->maxSocket = &maxSocket;
                    selectInfo->socketList = &socketList;
                    threadPool.addTask(readMsgTask, selectInfo);
                }
            }
        }
    }

    compatCloseSocket(serverSocket);
    
#ifdef _WIN32
    // windows 平台下结束了还需要移除网络库的装载
    WSACleanup();
#endif
}

void socketSelectDemo()
{
    // IO多路转接也称为IO多路复用，它是一种网络通信的手段（机制），通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的，
    // 一旦检测到有文件描述符就绪（可以读数据或者可以写数据）程序的阻塞就会被解除，之后就可以基于这些（一个或多个）就绪的文件描述符进行通信了。
    // 通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有：select、poll、epoll。

    // 对于 socket 并发编程存在多个阻塞点，这些组赛点相互互斥，导致需要使用多线程来解决，当并发两居高时会导致大量线程被开辟从而造成内存和系统资源开销影响服务器负荷。
    // 但其实中间有诸多客户端链接可能只是保持链接并没有过于频繁的操作，这样的线程开辟造成了资源的浪费，如果处理线程可以得到复用则会大大奖励这种负荷开销实现高性能并发。
    // 在 C++ socket 编程中提供了 select、poll、epoll 三种方式，其中，select、poll 内部使用链表维护，epoll 使用红黑树维护，所以：
    //  1. 在性能上 select、poll 要略逊于 epoll；
    //  2. 在可检测连接数上 select 也有限制，最大时 1024，超过 1024 就不行了（这个取决于系统配置），而 epoll、poll 没有这方面限制可以无限大；
    //  3. select 支持 windows 和 mac、linux；但 poll 和 epoll 并不支持 windows。
    // 注意：这里的可检测连接数并不是并发连接数，可以并发链接的数量取决于ip和端口组合数。这里是指在链接到服务器上的链接里去检测那些有传输数据那些没有。
    //      也就是说可以间接2000个客户端，但是超过1024后面的可能得不到检测。
    // 总结：所以我们需要着重理解 select 和 epoll 因为在 linux 下 epoll 有更好得优势，而考虑跨平台性时只能选择 select。

    // 下面先对多线程/多进程并发和 IO 多路转接的并发处理流程进行对比（服务器端）：
    // * 多线程/多进程并发
    //   - 主线程/父进程：调用 accept() 监测客户端连接请求:
    //     1. 如果没有新的客户端的连接请求，当前线程/进程会阻塞;
    //     2. 如果有新的客户端连接请求解除阻塞，建立连接;
    //   - 子线程/子进程：和建立连接的客户端通信:
    //     1. 调用 read() / recv() 接收客户端发送的通信数据，如果没有通信数据，当前线程/进程会阻塞，数据到达之后阻塞自动解除;
    //     2. 调用 write() / send() 给客户端发送数据，如果写缓冲区已满，当前线程/进程会阻塞，否则将待发送数据写入写缓冲区中.
    //
    // * IO多路转接并发
    //   - 使用IO多路转接函数委托内核检测服务器端所有的文件描述符（通信和监听两类），这个检测过程会导致进程/线程的阻塞，
    //     如果检测到已就绪的文件描述符阻塞解除，并将这些已就绪的文件描述符传出;
    //   - 根据类型对传出的所有已就绪文件描述符进行判断，并做出不同的处理:
    //     1. 监听的文件描述符：和客户端建立连接
    //        1) 此时调用 accept() 是不会导致程序阻塞的，因为监听的文件描述符是已就绪的（有新请求）
    //     2. 通信的文件描述符：调用通信函数和已建立连接的客户端通信
    //        1) 调用 read() / recv() 不会阻塞程序，因为通信的文件描述符是就绪的，读缓冲区内已有数据;
    //        2) 调用 write() / send() 不会阻塞程序，因为通信的文件描述符是就绪的，写缓冲区不满，可以往里面写数据
    //   - 对这些文件描述符继续进行下一轮的检测（循环往复。。。）
    //
    // 总结：与多进程和多线程技术相比，I/O多路复用技术的最大优势是系统开销小，系统不必创建进程/线程，也不必维护这些进程/线程，从而大大减小了系统的开销。

    // select
    // 函数原型
    //  #include <sys/select.h>
    //  struct timeval {
    //      time_t      tv_sec;         /* seconds */
    //      suseconds_t tv_usec;        /* microseconds */
    //  };
    //
    //  int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout);
    //
    // 使用 select 这种 IO 多路转接方式需要调用一个同名函数 select，这个函数是跨平台的，Linux、Mac、Windows 都是支持的。
    // 程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态，其实就是检测这些文件描述符对应的读写缓冲区的状态：
    //  * 读缓冲区：检测里边有没有数据，如果有数据该缓冲区对应的文件描述符就绪
    //  * 写缓冲区：检测写缓冲区是否可以写(有没有容量)，如果有容量可以写，缓冲区对应的文件描述符就绪
    //  * 读写异常：检测读写缓冲区是否有异常，如果有该缓冲区对应的文件描述符就绪
    //
    // 委托检测的文件描述符被遍历检测完毕之后，已就绪的这些满足条件的文件描述符会通过 select() 的参数分 3 个集合传出，程序猿得到这几个集合之后就可以分情况依次处理了。
    //
    // 函数参数：
    //  - int nfds：委托内核检测的这三个集合中最大的文件描述符 + 1
    //    * 内核需要线性遍历这些集合中的文件描述符，这个值是循环结束的条件
    //    * 在Window中这个参数是无效的，指定为-1即可
    //  - fd_set *readfds：文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区
    //    * 传入传出参数，读集合一般情况下都是需要检测的，这样才知道通过哪个文件描述符接收数据
    //  - fd_set *writefds：文件描述符的集合, 内核只检测这个集合中文件描述符对应的写缓冲区
    //    * 传入传出参数，如果不需要使用这个参数可以指定为NULL
    //  - fd_set *exceptfds：文件描述符的集合, 内核检测集合中文件描述符是否有异常状态
    //    * 传入传出参数，如果不需要使用这个参数可以指定为NULL
    //  - const PTIMEVAL timeout：超时时长，用来强制解除select()函数的阻塞的
    //    * NULL：函数检测不到就绪的文件描述符会一直阻塞。
    //    * 等待固定时长（秒）：函数检测不到就绪的文件描述符，在指定时长之后强制解除阻塞，函数返回 0
    //    * 不等待：函数不会阻塞，直接将该参数对应的结构体初始化为 0 即可。
    // 函数返回值：
    //  - 大于0：成功，返回集合中已就绪的文件描述符的总个数
    //  - 等于-1：函数调用失败
    //  - 等于0：超时，没有检测到就绪的文件描述符
    //
    // 注意：在操作系统中会为每个进程设置一个独立的文件描述符列表，该列表里保存了进程所建立的所有文件描述符，且文件描述符是连续递增累加的，在 C++ 中文件描述符（fd）是一个 int 型数据。
    //      本质上文件描述符只是一个索引，他对应着一块内存，我们可以通过这个索引获取到这块内存从而进行相关操作。需要注意的是虽然文件描述符新建是递增的，但是某些系统也存在对于回收文件
    //      描述符进行复用分配的情况。
    // 基于上面这个注意事项，加之 SOCKET 本质上就是一个文件描述符，他指向了一块读内存一块写内存。所以当我们拿到一个最大文件描述符时，我们就可以从 0 开始递增遍历到最大的文件描述符
    // 来完成对链接过来的 SOCKET 文件描述符的检查并操作。即便是存在回收重复利用他也在0到最大文件描述符内。只是这样的情况会存在不必要的建索，但性能影响并不大，如果追求极致，可以使用
    // list 来记录有效的文件描述符，从而减少遍历次数。
    //
    // 注意：如果某个文件描述符所指向内存区块存在内容，且没有被读取或处理，那么 select 检查的结果永远为准备状态，不管被调用多少次。这在使用多线程处理 select 的读取操作时需要相当注意。
    //      当一个文件描述符的内存是可读的，那么开启另一个线程去处理时，由于线程还没有得到时间片运行，并没有处理内存片区，而select所在线程进入第二次循环检查任然存在可读操作又会开启一个线程去处理
    //      实际上已经有一个任务在等待处理了，此时就会导致重复提交处理读取任务的线程。

    // fd_set
    // 结构体原型
    // typedef struct fd_set
    // {
    //      u_int fd_count;
    //     SOCKET fd_array[FD_SETSIZE];
    // } fd_set;
    // 文件描述符集合，用于传递给 select 自己所关心的文件描述符的集合。在 select 中一共需要传递 3 个文件描述符集合，分别是读取，写入，和异常。
    // select 会根据传入的文件描述符集合去检查对于文件描述符所指向内存区块的状态：
    //  * 读取文件描述符集合：只会在检查是否有消息传入时起作用，如果这个集合里没有出现的则不会检查读取内存区块状态。
    //  * 写入文件描述符集合：只会在检查是否已写满发送信息时起作用，如果这个集合里没有出现则不会检查写入内存区块状态。
    //  * 异常文件描述符集合：只会在检查发送异常时起作用，如果这里没有对应的文件描述符，则即便发生异常也不会检测。
    // 对于写入大部分情况都不会遇见发送数据的内存缓冲区写满的情况，所以一般我们不检查，会给NULL，异常也是一样没有需求也可以给NULL。
    // 
    // 文件描述符集合可以为空，我们可以把它看作一个map，当我们向里面添加一个文件描述符时，它会怎加一个标记位于文件描述符一一对应，这个标记位存在 0 或 1 两种状态。
    // 当文件描述符对应标记位为 0 时，则说明改文件描述符没有相关就绪状态，当状态等于 1 时，则说明有相关就绪状态，我们可以进行对应操作:
    // 如，读取文件描述符集合中出现文件描述符对应状态为 1 就可以进行 accept() 或 read() 等操作，因为已经是就绪态所以不会阻塞能够直接获取数据。其他的类推。
    //
    // 一些猜想：在 fd_set 里有2个数据一个 count，一个文件描述符数组，这个数组内如果存放了文件描述符就说明它的标记为 1 不存在则标记 0。这样来满足文件标记于状态的映射。
    //
    // 另外初始化fd_set类型的参数还需要使用相关的一些列操作函数，具体如下：
    //  * void FD_CLR(int fd, fd_set *set);     将文件描述符 fd 从 set 集合中删除   》 其效果等于 》    将 fd 对应的标志位设置为 0
    //  * int  FD_ISSET(int fd, fd_set *set);   判断文件描述符 fd 是否在 set 集合中 》 其效果等于 》    读一下 fd 对应的标志位到底是 0 还是 1
    //  * void FD_SET(int fd, fd_set *set);     将文件描述符 fd 添加到 set 集合中   》 其效果等于 》    将 fd 对应的标志位设置为 1
    //  * void FD_ZERO(fd_set *set);            将 set 集合中, 所有文件文件描述符对应的标志位设置为 0, 集合中没有添加任何文件描述符
    
    // 处理流程
    // 如果在服务器基于 select 实现并发，其处理流程如下：
    //  1. 创建监听的套接字 lfd = socket();
    //  2. 将监听的套接字和本地的IP和端口绑定 bind()
    //  3. 给监听的套接字设置监听 listen()
    //  4. 创建一个文件描述符集合 fd_set，用于存储需要检测读事件的所有的文件描述符
    //     * 通过 FD_ZERO() 初始化
    //     * 通过 FD_SET() 将监听的文件描述符放入检测的读集合中
    //  5. 循环调用select()，周期性的对所有的文件描述符进行检测
    //  6. select() 解除阻塞返回，得到内核传出的满足条件的就绪的文件描述符集合
    //     * 通过 FD_ISSET() 判断集合中的标志位是否为 1
    //       - 如果这个文件描述符是监听的文件描述符，调用 accept() 和客户端建立连接
    //         1) 将得到的新的通信的文件描述符，通过 FD_SET() 放入到检测集合中
    //       - 如果这个文件描述符是通信的文件描述符，调用通信函数和客户端通信
    //         1) 如果客户端和服务器断开了连接，使用FD_CLR()将这个文件描述符从检测集合中删除
    //         2) 如果没有断开连接，正常通信即可
    //  7. 重复第 6 步

    // 客户端不需要使用 IO 多路转接进行处理，因为客户端和服务器的对应关系是 1：N，也就是说客户端是比较专一的，只能和一个连接成功的服务器通信。
    // 虽然使用 select 这种 IO 多路转接技术可以降低系统开销，提高程序效率，但是它也有局限性：
    //  1. 待检测集合（第2、3、4个参数）需要频繁的在用户区和内核区之间进行数据的拷贝，效率低
    //  2. 内核对于 select 传递进来的待检测集合的检测方式是线性的
    //     * 如果集合内待检测的文件描述符很多，检测效率会比较低
    //     * 如果集合内待检测的文件描述符相对较少，检测效率会比较高
    //  3. 使用 select 能够检测的最大文件描述符个数有上限，默认是 1024，这是在内核中被写死了的。

    cout << "案例五：IO多路转接（复用）之select方案演示开始" << endl;
    cout << "请选择演示哪一种实现：" << endl;
    cout << "1. select 模型的简单单线程实现多路转接并发处理" << endl;
    cout << "2. select 模型的基于线程池复用多路转接并发处理" << endl;

    char commond;
    cin >> commond;
	switch (tolower(commond)) {
        case '1' :
            simpleSelectDemo();
            break;
        case '2' :
            threadPoolSelectDemo();
            break;
    }
}